/**
 * JavaScript call() 方法详细教程
 * 用途：调用一个函数并指定 this 的值以及传递参数列表
 * 语法：function.call(thisArg, arg1, arg2, ...)
 */

console.log('======= JavaScript call() 方法详细教程 =======\n')

// 1. call() 方法的基本概念
console.log('1. call() 方法的基本概念：')
console.log(
  'call() 方法允许调用一个函数，并显式指定该函数执行时的 this 值，以及传递参数。'
)
console.log('这是JavaScript中函数上下文控制的重要机制。')
console.log()

// 2. 基本用法 - 改变 this 指向
console.log('2. 基本用法 - 改变 this 指向：')

// 定义一个函数
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`)
}

// 创建两个对象
const person1 = { name: '张三' }
const person2 = { name: '李四' }

// 不使用 call()，此时 this 指向全局对象或 undefined（严格模式下）
console.log('不使用 call()：')
try {
  greet('你好', '!')
} catch (e) {
  console.log('错误：', e.message)
}

// 使用 call() 调用函数，指定 this 为 person1
console.log('使用 call() 指定 this 为 person1：')
greet.call(person1, '你好', '!') // 输出: 你好, 张三!

// 使用 call() 调用函数，指定 this 为 person2
console.log('使用 call() 指定 this 为 person2：')
greet.call(person2, '早上好', '。') // 输出: 早上好, 李四。
console.log()

// 3. call() 的核心作用：控制函数的执行上下文
console.log('3. call() 的核心作用：控制函数的执行上下文')

// 创建一个计算器对象
const calculator = {
  base: 10,
  add: function (a) {
    return this.base + a
  },
  subtract: function (a) {
    return this.base - a
  }
}

// 正常调用方法，this 指向 calculator
console.log('计算器正常调用：')
console.log(`calculator.add(5) = ${calculator.add(5)}`) // 15

// 创建另一个对象
const miniCalculator = {
  base: 1
}

// 使用 call() 借用 calculator 的方法
console.log('使用 call() 借用方法：')
console.log(
  `calculator.add.call(miniCalculator, 5) = ${calculator.add.call(
    miniCalculator,
    5
  )}`
) // 6
console.log(
  `calculator.subtract.call(miniCalculator, 1) = ${calculator.subtract.call(
    miniCalculator,
    1
  )}`
) // 0
console.log()

// 4. 构造函数继承中的应用
console.log('4. 构造函数继承中的应用：')

// 父类构造函数
function Animal(name) {
  this.name = name
  this.eat = function () {
    console.log(`${this.name} 正在吃东西`)
  }
}

// 子类构造函数
function Dog(name, breed) {
  // 使用 call() 调用父类构造函数，将 this 绑定到当前正在创建的 Dog 实例
  Animal.call(this, name)
  // 添加子类特有属性
  this.breed = breed
  this.bark = function () {
    console.log(`${this.name}(${this.breed}) 汪汪叫`)
  }
}

// 创建子类实例
const myDog = new Dog('旺财', '哈士奇')

// 验证实例属性和方法
console.log('子类实例：')
console.log(`名称: ${myDog.name}`) // 旺财
console.log(`品种: ${myDog.breed}`) // 哈士奇
myDog.eat() // 旺财 正在吃东西
myDog.bark() // 旺财(哈士奇) 汪汪叫
console.log()

// 5. 调用函数时不指定 this（传递 null 或 undefined）
console.log('5. 调用函数时不指定 this：')

function sum(a, b) {
  return a + b
}

// 传递 null 作为 this 值
console.log(`sum.call(null, 5, 3) = ${sum.call(null, 5, 3)}`) // 8

// 传递 undefined 作为 this 值
console.log(`sum.call(undefined, 10, 20) = ${sum.call(undefined, 10, 20)}`) // 30

// 注意：在严格模式下，this 会保持为 null 或 undefined
// 在非严格模式下，this 会指向全局对象
console.log()

// 6. 与 apply() 和 bind() 的对比
console.log('6. 与 apply() 和 bind() 的对比：')

function multiply(a, b) {
  return a * b
}

// call() - 参数逐个传递
console.log(`multiply.call(null, 4, 5) = ${multiply.call(null, 4, 5)}`) // 20

// apply() - 参数作为数组传递
console.log(`multiply.apply(null, [4, 5]) = ${multiply.apply(null, [4, 5])}`) // 20

// bind() - 返回绑定了 this 的新函数，不立即执行
const multiplyBy10 = multiply.bind(null, 10)
console.log(`bind() 创建的新函数: multiplyBy10(6) = ${multiplyBy10(6)}`) // 60
console.log()

// 7. call() 在实际开发中的高级应用
console.log('7. call() 在实际开发中的高级应用：')

// 7.1 检查对象类型的多种方法
console.log('7.1 检查对象类型：')

const arr = [1, 2, 3]
const obj = { a: 1 }
const func = function () {}

// 使用 Object.prototype.toString 和 call() 检查类型
function getType(value) {
  return Object.prototype.toString.call(value)
}

console.log(`arr 的类型: ${getType(arr)}`) // [object Array]
console.log(`obj 的类型: ${getType(obj)}`) // [object Object]
console.log(`func 的类型: ${getType(func)}`) // [object Function]
console.log(`null 的类型: ${getType(null)}`) // [object Null]
console.log(`undefined 的类型: ${getType(undefined)}`) // [object Undefined]
console.log()

// 7.2 借用数组方法操作类数组对象
console.log('7.2 借用数组方法操作类数组对象：')

// 模拟一个类数组对象（arguments）
function processArguments() {
  // 借用数组的 slice 方法将类数组对象转为真正的数组
  const argsArray = Array.prototype.slice.call(arguments)
  console.log(`转换前类型: ${typeof arguments}`)
  console.log(
    `转换后类型: ${Array.isArray(argsArray) ? 'Array' : typeof argsArray}`
  )

  // 借用数组的 forEach 方法遍历
  console.log('遍历参数:')
  Array.prototype.forEach.call(arguments, function (arg, index) {
    console.log(`第${index + 1}个参数: ${arg}`)
  })
}

processArguments('a', 'b', 'c')
console.log()

// 7.3 实现函数式编程中的上下文切换
console.log('7.3 函数式编程中的上下文切换：')

const mathOperations = {
  x: 10,
  y: 5,
  add: function () {
    return this.x + this.y
  },
  subtract: function () {
    return this.x - this.y
  }
}

const newContext = { x: 100, y: 50 }

// 在不同上下文中执行相同的函数
console.log(`原上下文 add(): ${mathOperations.add()}`) // 15
console.log(`新上下文 add(): ${mathOperations.add.call(newContext)}`) // 150
console.log(`原上下文 subtract(): ${mathOperations.subtract()}`) // 5
console.log(`新上下文 subtract(): ${mathOperations.subtract.call(newContext)}`) // 50
console.log()

// 7.4 实现对象的多重继承（混入模式）
console.log('7.4 对象的多重继承（混入模式）：')

// 创建几个特性对象
const canEat = {
  eat: function (food) {
    console.log(`${this.name} 正在吃 ${food}`)
  }
}

const canSleep = {
  sleep: function (hours) {
    console.log(`${this.name} 睡了 ${hours} 小时`)
  }
}

const canWork = {
  work: function (task) {
    console.log(`${this.name} 正在 ${task}`)
  }
}

// 创建一个人对象
const person = { name: '小明' }

// 使用 call() 和 Object.assign 实现混入
function mixin(target, ...sources) {
  sources.forEach((source) => {
    // 遍历源对象的每个方法
    Object.keys(source).forEach((key) => {
      // 将方法绑定到目标对象
      target[key] = source[key].call(target)
    })
  })
  return target
}

// 更简单的方式：直接复制方法
const employee = { name: '小红' }
Object.keys(canEat).forEach((key) => {
  employee[key] = function (...args) {
    return canEat[key].apply(this, args)
  }
})
Object.keys(canWork).forEach((key) => {
  employee[key] = function (...args) {
    return canWork[key].apply(this, args)
  }
})

employee.eat('午餐') // 小红 正在吃 午餐
employee.work('编写代码') // 小红 正在 编写代码
console.log()

// 8. 手写实现 call() 方法
console.log('8. 手写实现 call() 方法：')

// 模拟 call 方法的实现
Function.prototype.myCall = function (context = window, ...args) {
  // 确保 context 是一个对象，如果是原始值则包装
  context = context || window
  if (typeof context !== 'object') {
    context = new Object(context)
  }

  // 创建一个唯一属性名，避免覆盖原有属性
  const fnKey = Symbol('fn')

  // 将当前函数赋值给 context 的临时属性
  context[fnKey] = this

  // 调用函数，传入参数
  const result = context[fnKey](...args)

  // 删除临时属性
  delete context[fnKey]

  // 返回函数执行结果
  return result
}

// 测试手写的 myCall 方法
function testMyCall(x, y) {
  console.log(`this.name = ${this.name}, x + y = ${x + y}`)
  return x + y
}

const testObj = { name: '测试对象' }
console.log('使用原生 call():')
testMyCall.call(testObj, 10, 20)
console.log('使用手写 myCall():')
testMyCall.myCall(testObj, 10, 20)
console.log()

// 9. call() 方法的常见陷阱和注意事项
console.log('9. call() 方法的常见陷阱和注意事项：')

// 9.1 严格模式 vs 非严格模式的区别
console.log('9.1 严格模式 vs 非严格模式：')

// 非严格模式
function nonStrictMode() {
  console.log(`非严格模式下，this 为 null 或 undefined 时: ${this}`)
}

// 严格模式
function strictMode() {
  'use strict'
  console.log(`严格模式下，this 为 null 时: ${this}`)
  console.log(`严格模式下，this 为 undefined 时: ${this}`)
}

console.log('非严格模式测试:')
nonStrictMode.call(null) // 指向全局对象
nonStrictMode.call(undefined) // 指向全局对象

console.log('严格模式测试:')
try {
  strictMode.call(null) // 保持为 null
} catch (e) {
  console.log('错误:', e.message)
}
try {
  strictMode.call(undefined) // 保持为 undefined
} catch (e) {
  console.log('错误:', e.message)
}

// 9.2 原始值作为 thisArg 时的行为
console.log('9.2 原始值作为 thisArg：')

function showThisType() {
  console.log(`this 的类型: ${typeof this}`)
}

showThisType.call(123) // 数字被包装成 Number 对象
showThisType.call('hello') // 字符串被包装成 String 对象
showThisType.call(true) // 布尔值被包装成 Boolean 对象
console.log()

// 10. 总结
console.log('======= call() 方法总结 =======')
console.log('1. call() 方法允许调用一个函数并指定其执行时的 this 值')
console.log('2. 语法: function.call(thisArg, arg1, arg2, ...)')
console.log('3. 主要用途：')
console.log('   - 改变函数的执行上下文（this 指向）')
console.log('   - 实现构造函数的继承')
console.log('   - 借用其他对象的方法')
console.log('   - 操作类数组对象')
console.log('   - 实现函数式编程中的上下文切换')
console.log('4. call() 与 apply() 的区别：参数传递方式不同')
console.log('5. call() 与 bind() 的区别：call() 立即执行，bind() 返回新函数')
console.log('6. 在项目中，call() 常用于实现对象间的协作和代码复用')
